import babel from '@babel/core';
import traverse from '@babel/traverse';
import fs from 'fs';
import path from 'path';

const Traverse = traverse.default;

const readDirectoryContents = (directoryPath, models) => {
  const files = fs.readdirSync(directoryPath);

  for (const file of files) {
    const filePath = path.join(directoryPath, file);
    const stat = fs.statSync(filePath);

    if (stat.isFile()) {
      const content = fs.readFileSync(filePath, 'utf-8');
      const namespace = file.split('.')[0];

      if (models.some((model) => model.namespace === namespace)) {
        throw new Error(`Duplicate namespace in models: ${namespace}`);
      }

      //获取依赖，用babel 转化ast 得到useModel 函数的以一个参数
      const deps = new Set([]);
      const ast = babel.parseSync(content, {
        filename: file,
        sourceType: 'module',
        configFile: false,
        presets: ['@babel/preset-env', '@babel/preset-typescript'],
      });

      Traverse(ast, {
        CallExpression: (path) => {
          if (path.node.callee.name === 'useModel') {
            // 获取并保存参数
            deps.add(path.node.arguments[0].value);
          }
        },
      });

      const model = {
        namespace,
        filePath,
        content,
        deps: Array.from(deps),
      };

      models.push(model);
    } else if (stat.isDirectory) {
      readDirectoryContents(filePath);
    }
  }
};

const topologicalSort = (models) => {
  // build depts graph
  const graph = [];
  const namespaceToNode = {};

  models.forEach((model, index) => {
    const node = {
      namespace: model.namespace,
      deps: model.deps,
      index,
      in: 0,
      childs: [],
    };

    if (namespaceToNode[model.namespace]) {
      throw new Error(`Duplicate namespace in models: ${model.namespace}`);
    }
    namespaceToNode[model.namespace] = node;
    graph.push(node);
  });

  // build edges.
  graph.forEach((node) => {
    node.deps.forEach((dep) => {
      const depNode = namespaceToNode[dep];
      if (!depNode) {
        throw new Error(`Model namespace not found: ${dep}`);
      }
      depNode.childs.push(node);
      node.in++;
    });
  });

  const queue = [];
  while (true) {
    // find first 0 in node;
    const zeronode = graph.find((n) => {
      return n && n.in === 0;
    });
    if (!zeronode) {
      break;
    }

    queue.push(zeronode.namespace);
    zeronode.childs.forEach((child) => {
      child.in--;
    });
    zeronode.childs = [];
    delete graph[zeronode.index];
  }

  const leftNodes = graph.filter(Boolean);
  if (leftNodes.length > 0) {
    throw new Error(`Circle dependency detected in models`);
  }

  return queue;
};

export default function rollupPluginModels() {
  return {
    name: 'rollupPluginModels',

    resolveId(id) {
      console.log('resolveId', id);
    },
    watchChange(id, change) {
      console.log(id, change);
      if (id.includes('/src/models/') && change.event === 'create') {
        this.addWatchFile(id);
        this.load({
          id: id.split('/src/models/')[0] + '/src/main.tsx',
        });
      }
    },
    load(id, options) {
      if (id.endsWith('main.tsx')) {
        const content = fs.readFileSync(id);
        const modelsPath = path.dirname(id) + '/models';
        const models = [];
        readDirectoryContents(modelsPath, models);

        const namespaces = topologicalSort(models);

        models.sort(
          (a, b) =>
            namespaces.indexOf(a.namespace) - namespaces.indexOf(b.namespace),
        );

        const modelImports =
          models
            .map((model) => {
              return `import ${model.namespace} from "${model.filePath}";`;
            })
            .join('\n') + `import { Provider } from "./provider";`;

        const contentAfterImport = modelImports + content;

        const finalContent = contentAfterImport.replace(
          /<App\s*\/>/g,
          `<Provider models={{
            ${namespaces.join(',')}
        }}>$&</Provider>`,
        );
        console.log(finalContent);
        return finalContent;
      }
    },
    transform(code, id, options) {
      // console.log("transform", id);
    },
    options(options) {
      // console.log("options", options);
    },
  };
}
